﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using IEx.Common;
using IEx.Model.Partials;
using IEx.Model.Partials.Media;
using IEx.Utilities;
using Newtonsoft.Json.Linq;

namespace IEx.ParserBuilder.Youtube
{
    public class YoutubeParser : IHtmlParser
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="item"></param>
        /// <param name="html"></param>
        /// <param name="attachResultItem"></param>
        /// <param name="attachNewItemSource"></param>
        public void Parse(SourceItem item, string html, Action<Model.Partials.Announcement> attachResultItem, Action<SourceItem> attachNewItemSource)
        {
            try
            {
                List<Announcement> detectedAnnouncements = new List<Announcement>();
                Uri newUri = Utility.CreateNewUriForNextPage(item.Url);

                if (item.Url.IndexOf("?") == -1)
                {
                    string firstThreadTitle = "id=\"feed-main-what_to_watch\"";
                    string lastThreadTitle = "id=\"feed-error\"";
                    string remainHtml = Utility.GetHtmlBody(html, firstThreadTitle, lastThreadTitle);

                    if (!string.IsNullOrEmpty(remainHtml))
                    {
                        string videoUrl = string.Empty;
                        Regex regVideoUrl = new Regex("<a href=\".*?\" class=\"ux-thumb-wrap yt-uix-sessionlink .*?\".*?>.*?</a>");
                        MatchCollection mVideoUrl = regVideoUrl.Matches(remainHtml);
                        for (int i = 0; i < mVideoUrl.Count; i++)
                        {
                            string videoUrlHtml = mVideoUrl[i].Value;
                            string href = RegexUtility.ExtractHyperlink(videoUrlHtml);
                            videoUrl = new Uri(newUri, href).ToString();
                            List<VideoInfo> videoInfos = new List<VideoInfo>();
                            List<Comment> comments = new List<Comment>();
                            Announcement annDetail = new Announcement();
                            if (!string.IsNullOrEmpty(videoUrl))
                            {
                                bool decryptSignature = false;
                                videoInfos = ExtractMediaInfo(videoUrl, item, ref comments, ref annDetail, decryptSignature).ToList();
                            }
                            if (!string.IsNullOrEmpty(videoUrl) && videoInfos.Count > 0)
                            {
                                Announcement ann = new Announcement();
                                ann.SourceId = item.SourceId;
                                ann.SourceCode = item.SourceCode;
                                ann.DateNumberic = annDetail.DateNumberic;
                                ann.DateTime = annDetail.DateTime;
                                ann.Message = annDetail.Message;
                                ann.SocialNetwork.ScreenName = annDetail.SocialNetwork.ScreenName;
                                ann.Title = annDetail.Title;
                                ann.Link = videoUrl;
                                ann.LangId = item.LangId;
                                ann.InsertedDate = DateTime.Now;
                                ann.Attachment = true;
                                ann.MessageTypeId = 0;
                                ann.SocialNetwork.Videos = videoInfos;
                                detectedAnnouncements.Add(ann);
                            }
                        }
                    }
                }
                else
                {
                    string firstThreadTitle = "id=\"results\"";
                    string lastThreadTitle = "id=\"search-secondary-col-contents\"";
                    string remainHtml = Utility.GetHtmlBody(html, firstThreadTitle, lastThreadTitle);
                    if (!string.IsNullOrEmpty(remainHtml))
                    {
                        string videoUrl = string.Empty;
                        Regex regVideoUrl = new Regex("<a href=\".*?\" class=\"contains-addto yt-uix-sessionlink.*?\".*?>.*?</a>");
                        MatchCollection mVideoUrl = regVideoUrl.Matches(remainHtml);
                        for (int i = 0; i < mVideoUrl.Count; i++)
                        {
                            string videoUrlHtml = mVideoUrl[i].Value;
                            string href = RegexUtility.ExtractHyperlink(videoUrlHtml);
                            videoUrl = new Uri(newUri, href).ToString();
                            List<VideoInfo> videoInfos = new List<VideoInfo>();
                            List<Comment> comments = new List<Comment>();
                            Announcement annDetail = new Announcement();
                            if (!string.IsNullOrEmpty(videoUrl))
                            {
                                bool decryptSignature = false;
                                videoInfos = ExtractMediaInfo(videoUrl, item, ref comments, ref annDetail, decryptSignature).ToList();
                            }
                            if (!string.IsNullOrEmpty(videoUrl) && videoInfos.Count > 0)
                            {
                                Announcement ann = new Announcement();
                                ann.SourceId = item.SourceId;
                                ann.SourceCode = item.SourceCode;
                                ann.DateNumberic = annDetail.DateNumberic;
                                ann.DateTime = annDetail.DateTime;
                                ann.Message = annDetail.Message;
                                ann.SocialNetwork.ScreenName = annDetail.SocialNetwork.ScreenName;
                                ann.Title = annDetail.Title;
                                ann.Link = videoUrl;
                                ann.SocialNetwork.Videos = videoInfos;
                                ann.LangId = item.LangId;
                                ann.InsertedDate = DateTime.Now;
                                ann.Attachment = true;
                                ann.MessageTypeId = 0;
                                detectedAnnouncements.Add(ann);
                            }
                        }
                        //Extract url next page
                        Regex regNextPage = new Regex("class=\"yt-uix-button  yt-uix-pager-button yt-uix-button-toggled yt-uix-sessionlink yt-uix-button-default yt-uix-button-size-default\".*?</a>.*?<a.*?>.*?</a>");
                        Match mNextPage = regNextPage.Match(remainHtml);
                        if (mNextPage.Success)
                        {
                            string nextPageHtml = mNextPage.Value.Trim();
                            string link = RegexUtility.ExtractHyperlink(nextPageHtml);
                            item.Url = new Uri(newUri, link).ToString();
                            item.Url = string.Empty;//conditional for debug
                        }
                        else
                        {
                            item.Url = string.Empty;
                        }
                    }
                    if (!string.IsNullOrEmpty(item.Url))
                    {
                        SourceItem nItem = new SourceItem(item.Url, item.LangId, item.SourceCode, item.SearchCode, item.SourceId, item.DateTimeFormat);
                        if (!string.IsNullOrEmpty(item.RequestBuilderType))
                            nItem.RequestBuilderType = item.RequestBuilderType;

                        if (!string.IsNullOrEmpty(item.HtmlParserType))
                            nItem.HtmlParserType = item.HtmlParserType;

                        nItem.PrevResponseHtml = html;
                        attachNewItemSource(nItem);
                    }
                }
                // insert into download queue when no error in parsing
                foreach (Announcement ann in detectedAnnouncements)
                {
                    attachResultItem(ann);
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        #region Extract media file from Youtube

        /// <summary>
        /// Normalizes the given YouTube URL to the format http://youtube.com/watch?v={youtube-id}
        /// and returns whether the normalization was successful or not.
        /// </summary>
        /// <param name="url">The YouTube URL to normalize.</param>
        /// <param name="normalizedUrl">The normalized YouTube URL.</param>
        /// <returns>
        /// <c>true</c>, if the normalization was successful; <c>false</c>, if the URL is invalid.
        /// </returns>
        private static bool TryNormalizeYoutubeUrl(string url, out string normalizedUrl)
        {
            url = url.Trim();

            url = url.Replace("youtu.be/", "youtube.com/watch?v=");
            url = url.Replace("www.youtube", "youtube");
            url = url.Replace("youtube.com/embed/", "youtube.com/watch?v=");

            if (url.Contains("/v/"))
            {
                url = "http://youtube.com" + new Uri(url).AbsolutePath.Replace("/v/", "/watch?v=");
            }

            url = url.Replace("/watch#", "/watch?");

            IDictionary<string, string> query = Utility.ParseQueryString(url);

            string v;

            if (!query.TryGetValue("v", out v))
            {
                normalizedUrl = null;
                return false;
            }

            normalizedUrl = "http://youtube.com/watch?v=" + v;

            return true;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="url"></param>
        /// <param name="item"></param>
        /// <param name="decryptSignature"></param>
        /// <returns></returns>
        private static IEnumerable<VideoInfo> ExtractMediaInfo(string url, SourceItem item, ref List<Comment> comments, ref Announcement ann, bool decryptSignature = true)
        {
            Logger.Debug("***SCANNING EXTRACT VIDEO URL DETAIL: " + url);
            if (string.IsNullOrEmpty(url))
            {
                throw new Exception();
            }
            bool isYoutubeUrl = TryNormalizeYoutubeUrl(url, out url);
            if (!isYoutubeUrl)
            {
                throw new Exception();
            }
            try
            {
                var json = LoadJson(url, ref comments, ref ann);
                if (json != null)
                {
                    string videoTitle = GetVideoTitle(json);
                    ann.Title = videoTitle;
                    IEnumerable<ExtractionInfo> downloadUrls = ExtractDownloadUrls(json);

                    IEnumerable<VideoInfo> infos = GetVideoInfos(downloadUrls, videoTitle).ToList();

                    string htmlPlayerVersion = GetHtml5PlayerVersion(json);

                    foreach (VideoInfo info in infos)
                    {
                        info.HtmlPlayerVersion = htmlPlayerVersion;

                        if (decryptSignature && info.RequiresDecryption)
                        {
                            Utility.DecryptDownloadUrl(info);
                        }
                    }

                    return infos;
                }
                // if this request is fail request in previous scanning, 
                // remove it when success in current scanning
                if (item.FailId > 0)
                    Utility.DeleteFailRequests(item.FailId);

                Logger.Debug("***END SCANNING EXTRACT VIDEO URL DETAIL: " + url);
            }
            catch (Exception ex)
            {
                if (item.FailId == 0)
                {
                    item.FailId = DateTime.Now.Ticks;
                    Utility.SaveFailRequests(item);
                }
                // then write log
                string message = string.Format(@"
            Source code:   {0}
            HTML Parsing:   Fail
            Request URL:    {1}
            Search key:     {2}", item.SourceCode, url, item.SearchCode);
                Logger.WriteError(message, ex);
                return null;
            }
            return null;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="url"></param>
        /// <returns></returns>
        private static JObject LoadJson(string url, ref List<Comment> comments, ref Announcement ann)
        {
            HttpStatusCode status = HttpStatusCode.Accepted;
            string pageSource = Utility.LoadHtml(url, ref status);

            if (!string.IsNullOrEmpty(pageSource))
            {
                var dataRegex = new Regex(@"ytplayer\.config\s*=\s*(\{.+?\});", RegexOptions.Multiline);
                string extractedJson = dataRegex.Match(pageSource).Result("$1");
                pageSource = string.Join(string.Empty, Regex.Split(pageSource, @"(?:\r\n|\n|\r|\t)"));
                string desc = string.Empty;
                string user = string.Empty;
                string dateTime = string.Empty;
                DateTime postDate = DateTime.MinValue;
                Regex regDescription = new Regex("<p id=\"eow-description\"\\s>.*?</p>", RegexOptions.Multiline);
                Match mDescription = regDescription.Match(pageSource);
                if (mDescription.Success)
                {
                    string descHtml = mDescription.Value.Trim();
                    desc = RegexUtility.ExtractTextFromHtmlTag(descHtml);
                }
                Regex regUser = new Regex("<div class=\"yt-user-info\">.*?</div>", RegexOptions.Multiline);
                Match mUser = regUser.Match(pageSource);
                if (mUser.Success)
                {
                    string userHtml = mUser.Value.Trim();
                    user = RegexUtility.ExtractTextFromHtmlTag(userHtml);
                }
                Regex regDateTime = new Regex("<strong class=\"watch-time-text\">.*?</strong>", RegexOptions.Multiline);
                Match mDateTime = regDateTime.Match(pageSource);
                if (mDateTime.Success)
                {
                    string dateTimeHtml = mDateTime.Value.Trim();
                    dateTime = RegexUtility.ExtractTextFromHtmlTag(dateTimeHtml);
                    postDate = Utility.ConvertVnDateTimeObject(dateTime, "dd/MM/yyyy HH:mm");
                }
                ann.Message = desc;
                ann.SocialNetwork.ScreenName = user;
                ann.DateTime = postDate;
                ann.DateNumberic = long.Parse(postDate.ToString("yyyyMMddHHmmss"));
                comments.Add(new Comment()
                {
                    //Need to extract comments
                });

                return JObject.Parse(extractedJson);
            }
            return null;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="json"></param>
        /// <returns></returns>
        private static string GetVideoTitle(JObject json)
        {
            JToken title = json["args"]["title"];

            return title == null ? String.Empty : title.ToString();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="json"></param>
        /// <returns></returns>
        private static IEnumerable<ExtractionInfo> ExtractDownloadUrls(JObject json)
        {
            string[] splitByUrls = GetStreamMap(json).Split(',');
            string[] adaptiveFmtSplitByUrls = GetAdaptiveStreamMap(json).Split(',');
            splitByUrls = splitByUrls.Concat(adaptiveFmtSplitByUrls).ToArray();

            foreach (string s in splitByUrls)
            {
                IDictionary<string, string> queries = Utility.ParseQueryString(s);
                string url;
                bool requiresDecryption = false;

                if (queries.ContainsKey("s") || queries.ContainsKey("sig"))
                {
                    requiresDecryption = queries.ContainsKey("s");
                    string signature = queries.ContainsKey("s") ? queries["s"] : queries["sig"];

                    url = string.Format("{0}&{1}={2}", queries["url"], Utility.GetSignatureQuery(), signature);

                    string fallbackHost = queries.ContainsKey("fallback_host") ? "&fallback_host=" + queries["fallback_host"] : String.Empty;

                    url += fallbackHost;
                }

                else
                {
                    url = queries["url"];
                }

                url = HttpUtility.UrlDecode(url);
                url = HttpUtility.UrlDecode(url);

                yield return new ExtractionInfo { RequiresDecryption = requiresDecryption, Uri = new Uri(url) };
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="json"></param>
        /// <returns></returns>
        private static string GetStreamMap(JObject json)
        {
            try
            {
                JToken streamMap = json["args"]["url_encoded_fmt_stream_map"];

                string streamMapString = streamMap == null ? null : streamMap.ToString();

                if (streamMapString == null || streamMapString.Contains("been+removed"))
                {
                    Logger.Debug("***Video is removed or has an age restriction.");
                }
                return streamMapString;
            }
            catch (Exception ex)
            {
                Logger.WriteError("Video is removed or has an age restriction.", ex);
            }

            return null;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="json"></param>
        /// <returns></returns>
        private static string GetAdaptiveStreamMap(JObject json)
        {
            JToken streamMap = json["args"]["adaptive_fmts"];

            return streamMap.ToString();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="extractionInfos"></param>
        /// <param name="videoTitle"></param>
        /// <returns></returns>
        private static IEnumerable<VideoInfo> GetVideoInfos(IEnumerable<ExtractionInfo> extractionInfos, string videoTitle)
        {
            var downLoadInfos = new List<VideoInfo>();

            foreach (ExtractionInfo extractionInfo in extractionInfos)
            {

                string itag = Utility.ParseQueryString(extractionInfo.Uri.Query)["itag"];

                int formatCode = !string.IsNullOrEmpty(itag) ? int.Parse(itag) : 0;

                VideoInfo info = VideoInfo.Defaults.SingleOrDefault(videoInfo => videoInfo.FormatCode == formatCode);

                if (info != null)
                {
                    info = new VideoInfo(info)
                    {
                        DownloadUrl = extractionInfo.Uri.ToString(),
                        Title = videoTitle,
                        RequiresDecryption = extractionInfo.RequiresDecryption
                    };
                }

                else
                {
                    info = new VideoInfo(formatCode)
                    {
                        DownloadUrl = extractionInfo.Uri.ToString()
                    };
                }

                downLoadInfos.Add(info);
            }

            return downLoadInfos;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="json"></param>
        /// <returns></returns>
        private static string GetHtml5PlayerVersion(JObject json)
        {
            var regex = new Regex(@"html5player-(.+?)\.js");

            string js = json["assets"]["js"].ToString();

            return regex.Match(js).Result("$1");
        }

        #endregion
    }
}
